/*
 * Copyright 2006 Matt Jensen
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.spectaql.internal.proxy.cglib;

import net.sf.cglib.proxy.CallbackFilter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.spectaql.internal.util.ClassUtils;
import org.spectaql.metadata.EntityMetadata;
import org.spectaql.metadata.PropertyMetadata;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
 * <p>
 * <code>ProxyCallbackFilter</code> is an implementation of the CGLib {@link CallbackFilter} interface which maps
 * methods to their corresponding entries in a callback array. The indices in this array must correspond to the
 * following callback types:
 * <ol start="0">
 * <li>
 * Callback which handles invocations of {@link Object#equals(Object)}.
 * </li>
 * <li>
 * Callback which handles invocations of {@link Object#toString()}.
 * </li>
 * <li>
 * Callback which handles invocations of entity property accessor methods.
 * </li>
 * <li>
 * Callback which handles invocations of methods declared on the internal interface which was passed to the constructor. 
 * </li>
 * <li>
 * Callback which passes invocations of all other methods to the proxied superclass.
 * </li>
 * </ol>
 * </p>
 * <p>
 * Because the callback indices must correspond to entries in arrays which are constructed elsewhere, it is important
 * that this class stay in sync with {@link CglibProxyInstantiationStrategy}. There is no way to ensure that consistency
 * is maintained using compile-time checks.
 * </p>
 * <p>
 * <strong>Thread Safety:</strong> Instances of this class are thread safe.
 * </p>
 */
class ProxyCallbackFilter implements CallbackFilter
{
    private static Log LOG = LogFactory.getLog(ProxyCallbackFilter.class);

    /**
     * <p>
     * <code>CallbackType</code> defines the callback indices supported by this filter.
     * </p>
     */
    enum CallbackType
    {
        /**
         * <p>
         * The method is {@link Object#equals(Object)}.
         * </p>
         */
        EQUALS,

        /**
         * <p>
         * The method is {@link Object#toString()}.
         * </p>
         */
        TOSTRING,

        /**
         * <p>
         * The method is an entity property accessor.
         * </p>
         */
        PROPERTY,

        /**
         * <p>
         * THe method is declared on the mixed-in internal interface.
         * </p>
         */
        INTERNAL,

        /**
         * <p>
         * The method is a pass-through; it fits no other category.
         * </p>
         */
        PASSTHROUGH;
    };

    /**
     * <p>
     * Entity metadata used to recognize property accessor methods.
     * </p>
     */
    private final Set<Method> m_accessorMethods;

    /**
     * <p>
     * The mixed-in internal interface. 
     * </p>
     */
    private final Class<?> m_internalInterface;

    /**
     * <p>
     * Construct a {@link ProxyCallbackFilter} instance.
     * </p>
     *
     * @param entityMetadata entity metadata used to recognize property accessor methods.
     * @param internalInterface the internal interface which is to be mixed into the proxy.
     */
    ProxyCallbackFilter(final EntityMetadata entityMetadata, final Class<?> internalInterface)
    {
        super();
        assert null != entityMetadata : "The [entityMetadata] argument cannot be null.";
        assert null != internalInterface : "The [internalInterface] argument cannot be null.";
        assert internalInterface.isInterface() : "The [internalInterface] argument must be an interface.";

        m_accessorMethods = createAccessorMethodsSet(entityMetadata);
        m_internalInterface = internalInterface;
    }

    /* inherit javadoc */
    public int accept(final Method method)
    {
        // Determine the callback type.
        final ProxyCallbackFilter.CallbackType callbackType;
        if (ClassUtils.isMethodPresentOnClass(method, Object.class))
        {
            final String name = method.getName();
            if ("equals".equals(name))
            {
                callbackType = CallbackType.EQUALS;
            }
            else if ("toString".equals(name))
            {
                callbackType = CallbackType.TOSTRING;
            }
            else
            {
                callbackType = CallbackType.PASSTHROUGH;
            }
        }
        else if (ClassUtils.isMethodPresentOnClass(method, m_internalInterface))
        {
            callbackType = CallbackType.INTERNAL;
        }
        else if (m_accessorMethods.contains(method))
        {
            callbackType = ProxyCallbackFilter.CallbackType.PROPERTY;
        }
        else
        {
            callbackType = ProxyCallbackFilter.CallbackType.PASSTHROUGH;
        }
        return callbackType.ordinal();
    }

    /**
     * <p>
     * Extract all property accessor methods from an entity metadata object and return them as a set.
     * </p>
     *
     * @param entityMetadata the entity metadata.
     * @return {@link Set} of {@link Method} instances.
     */
    private Set<Method> createAccessorMethodsSet(final EntityMetadata entityMetadata)
    {
        // Get all properties and allocate a set large enough to hold a getter and setter for each.
        final Collection<PropertyMetadata> propertyMetadatas = entityMetadata.getProperties();
        final Set<Method> accessorMethods = new HashSet<Method>(propertyMetadatas.size() * 2);
        for (final PropertyMetadata propertyMetadata : propertyMetadatas)
        {
            final Method readMethod = propertyMetadata.getReadMethod();
            if (null != readMethod)
            {
                accessorMethods.add(readMethod);
            }
            final Method writeMethod = propertyMetadata.getWriteMethod();
            if (null != writeMethod)
            {
                accessorMethods.add(writeMethod);
            }
        }
        return accessorMethods;
    }
}
